#!/usr/bin/python

from Util import *
import os, sys

class Vault(object):

    def __init__(self, path, info):
        self.__dir = Dir(path)
        self.__git = Git(path)
        self.__info = info

        if self.__git.init_if_new():
            self.__init_repo()

        self.blob_dir = Dir(self.__git.storage, 'vault-blobs')

    @property
    def path(self):
        return str(self.__dir)

    def __execute(self, status):
        op = self.__git.add

        if status.tree == 'delete':
            op = self.__git.rm
        elif status.tree == 'unmerge':
            error("Unmerged, conflict?")
        op(status, path)

    def __init_repo(self):
        trace("New repository, tagging it")
        mark = '.vault'
        cleanup = [lambda: shutil.rmtree(self.__git.storage)]
        try:
            self.__git.config("user.email", self.__info["email"])
            self.__git.config("user.name", self.__info["user"])

            mark_fname = os.path.join(self.path, mark)
            with open(mark_fname, 'w') as f:
                cleanup.append(lambda: os.remove(mark_fname))
                f.write('{}'.format(str(datetime.now())))
            self.__git.add(mark)
            self.__git.commit('-m', 'vault created')
        except Exception as e:
            trace("Error {} on tagging", e)
            [f() for f in cleanup]
            raise e
        self.__git.config('status.showUntrackedFiles', 'all')

    def abspath(self, git_path):
        return os.path.join(self.path, git_path)

    def blob_paths(self, blob_hash):
        d = self.blob_dir.subdir(blob_hash[0:2])
        return str(d), d.subpath(blob_hash[2:])

    def hash_object(self, path):
        out, err, res = self.__git.hash_object(path)
        return None if res else out.split('\n')[0]

    def backup(self, modules):
        self.__backup_time = datetime.now()
        [self.__backup_module(m) for m in modules]
        self.__git.tag(date_iso(self.__backup_time))

    def __commit(self, msg, *args, **kwargs):
        out, err, res = self.__git.commit('-m', msg, *args, **kwargs)

    def __verify_added(self, path):
        for s in self.status(path):
            if not s.tree is None:
                error("Item {} has not been added to index", s)

    def __verify_clean(self, path):
        for s in self.status(path):
            if not (s.tree is None and s.idx is None):
                error("Item {} is not commited", s)

    def __add_blob(self, item):
        path = self.abspath(item.src)
        ahash = self.hash_object(path)
        blob_dir, blob_name = self.blob_paths(ahash)
        is_new_blob = False
        if not os.path.isdir(blob_dir):
            os.path.mkdir(blob_dir)
            is_new_blob = True
        elif not os.path.isfile(blob_name):
            is_new_blob = True

        if is_new_blob:
            os.rename(path, blob_name)
        else:
            os.unlink(path)

        target = os.path.relpath(blob_name, os.path.dirname(path))
        os.symlink(target, path)

    def __save_blobs(self, module, now):
        path = module.blob_ln_path
        blob_state = self.status(module.blob_ln_path)
        [self.__add_blob(item) for item in blob_state]

    def __backup_module(self, module):
        trace('Commiting {}', module)
        now = self.__backup_time
        module.set_vault(self.path)
        try:
            module.invoke()
            self.__save_blobs(module, now)

            path = str(module.root_dir)
            self.__git.add('-A', path)
            self.__verify_added(path)
            self.__commit('{} {}'.format(module, str(now)))
            self.__verify_clean(path)

        except Exception as e:
            print "Error backing up {}".format(module)
            print traceback.print_exc()
            self.__rollback(module)
            raise e

    def __rollback(self, module):
        trace("Rolling back {}", str(module))
        self.__git.reset('--hard')
        self.__git.clean('-fd')

    def status(self, path):
        return Status.get(self.__git, path)

class ModuleDir(Dir):
    def __init__(self, *path):
        super(ModuleDir, self).__init__(*path)

class Module(object):
    def __init__(self, home_dir, name, script, *params):
        self.name = name
        self.__home = home_dir
        self.script = os.path.abspath(script)
        os.path.isfile(self.script) or error("Not file {}", script)
        self.params = params

    def __str__(self):
        return self.name

    def __repr__(self):
        return 'Module({}, {}, {})'\
            .format(self.name, self.script, ', '.join(self.params))

    def set_vault(self, vault_dir):
        trace("Module: {}", self.name)
        self.vault_dir = vault_dir
        self.root_dir = ModuleDir(vault_dir, self.name)
        self.data_path = os.path.join(str(self.root_dir), 'data')
        self.blob_ln_path = os.path.join(str(self.root_dir), 'blobs')

    def invoke(self):
        trace("Invoking {} backup", self.name)
        rmtree(self.data_path)
        rmtree(self.blob_ln_path)

        out, err, res = shell("..", self.script,
                              '--action', 'export',
                              '--dir', self.data_path,
                              '--bin-dir', str(self.blob_ln_path),
                              '--home-dir', str(self.__home),
                              *self.params)
        res and error("Non-zero return code {}\nSTDOUT:\n{}STDERR:\n{}\n",
                      res, out, err)

    def rm(self):
        trace("Removing {} tree", self.name)
        [shutil.rmtree(d) for d in \
             [self.data_dir, self.blob_dir, self.blob_ln_dir]]

if __name__ == '__main__':
    vault_dir, home_dir = [os.path.abspath(os.path.expanduser(p)) \
                               for p in sys.argv[1:]]

    # TODO modules configuration should be passed in command line
    modules = [
        Module(home_dir, 'openclipart', '../examples/scripts/openclipart.py'),
        Module(home_dir, 'contacts', '../examples/scripts/contacts.py')
        ]

    # TODO user info should be passed from outside
    vault = Vault(vault_dir, {"user" : "Test User",
                              "email" : "somebody@nowhere.com"})
    vault.backup(modules)
